package org.piax.rospubsub;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * ADB forward を使った Android との通信路
 */
public class ADBBridge {
    public static int DEFAULT_BUF_SIZE = 8*1024;

    private SocketChannel soc = null;
    private ByteBuffer sendBuf = null;
    private ByteBuffer recvBuf = null;
    private ADBBridgeCallback callback = null;
    private volatile boolean threadLoop = false;
    private Thread recvThread = null;

    /**
     * Android 側からメッセージを受け取った場合に呼ばれる callback
     */
    public interface ADBBridgeCallback {
        /**
         * Android 側からメッセージを受け取った場合に呼ばれる callback
         * @param bridge メッセージを受け取った ADBBridge
         * @param msg Android 側から受け取った ADBBridgeMessage
         */
        void callback(ADBBridge bridge, ADBBridgeMessage msg);
    }

    /**
     * コンストラクタ
     */
    public ADBBridge() {
        this(DEFAULT_BUF_SIZE);
    }

    /**
     * コンストラクタ
     * @param buf_size 送受信バッファサイズ このバッファサイズがメッセージサイズの上限
     */
    public ADBBridge(int buf_size) {
        if (buf_size < 7)
            throw new IllegalArgumentException("buf_size should be greater than 6");

        sendBuf = ByteBuffer.allocate(buf_size);
        recvBuf = ByteBuffer.allocate(buf_size);
    }

    /**
     * コンストラクタ
     * 
     * ServerADBBridge から呼ばれる
     * @param soc accepted socket
     * @param callback Android 側からメッセージを受け取った場合に呼ばれる callback
     * @param buf_size 送受信バッファサイズ このバッファサイズがメッセージサイズの上限
     */
    protected ADBBridge(SocketChannel soc, ADBBridgeCallback callback, int buf_size) {
        this(buf_size);
        if (soc == null)
            throw new IllegalArgumentException("soc should not be null");
        if (!soc.isConnected())
            throw new IllegalArgumentException("soc should be connected");
        if (callback == null)
            throw new IllegalArgumentException("callback should not be null");

        this.callback = callback;
        threadLoop = true;
        sendBuf.clear();
        recvBuf.clear();
        this.soc = soc;
        recvThread = new RecvThread();
        recvThread.start();
    }

    /**
     * ADB forward を介して Android との接続を確立する。
     * 
     * @param adb_fwd ADB forward のPC側接続先
     * @param callback Android 側からメッセージを受け取った場合に呼ばれる callback
     * @throws IOException 接続に失敗した場合
     */
    public synchronized void open(InetSocketAddress adb_fwd, ADBBridgeCallback callback) throws IOException {
        if (adb_fwd == null)
            throw new IllegalArgumentException("adb_fwd should not be null");
        if (adb_fwd.isUnresolved())
            throw new IllegalArgumentException("adb_fwd should be resolved");

        if (threadLoop == true)
            throw new IllegalStateException("ADBBridge is already started.");

        this.callback = callback;
        threadLoop = true;
        sendBuf.clear();
        recvBuf.clear();
        soc = SocketChannel.open(adb_fwd);
        recvThread = new RecvThread();
        recvThread.start();
    }

    /**
     * 接続を閉じる。
     * @throws IOException 
     * @throws InterruptedException 
     */
    public synchronized void close() throws IOException, InterruptedException {
        if (!threadLoop)
            return;

        threadLoop = false;
        soc.close();
        recvThread.interrupt();
        recvThread.join();
        callback = null;
        recvThread = null;
    }

    /**
     * ADB Bridge の接続状態を取得する。
     * @return
     */
    public boolean isConnected() {
        return soc.isConnected();
    }

    /**
     * メッセージを送信する。
     * @param msg 送信するメッセージ
     * @throws IOException 送信に失敗した場合
     */
    public synchronized void send(ADBBridgeMessage msg) throws IOException {
        if (soc == null)
            throw new IllegalStateException("ADBBridge is not connected.");
        if (msg == null)
            throw new IllegalArgumentException("msg should not be null");

        sendBuf.clear();
        msg.serialize(sendBuf);
        sendBuf.flip();
        while (sendBuf.hasRemaining()) {
            soc.write(sendBuf);
        }
    }

    /**
     *  受信スレッド
     */
    private class RecvThread extends Thread {
        @Override
        public void run() {
            recvBuf.clear();
            while (threadLoop) {
                try {
                    if (soc.read(recvBuf) == -1) {
                        // socket closed
                        threadLoop = false;
                        break;
                    }

                    recvBuf.flip();
                    // メッセージを読める場合は読み込む
                    // そうでない場合は途中まで受信した状態なので続きを待つ
                    if  (ADBBridgeMessage.deserializable(recvBuf)) {
                        while (ADBBridgeMessage.deserializable(recvBuf)) {
                            if (callback != null)
                                callback.callback(ADBBridge.this, ADBBridgeMessage.deserialize(recvBuf));
                        }
                        recvBuf.compact();
                    }
                    recvBuf.limit(recvBuf.capacity());
                } catch (IOException e) {
                    if (threadLoop) {
                        // recvLoop == true で IOException は切断などの例外事項
                        e.printStackTrace();
                        threadLoop = false;
                    }
                    // recvLoop == false での IOException は正常停止時
                }
            }
        }
    }
}
